查看原文
其他

如何使用管道操作符优雅的书写R语言代码

杜雨 R语言中文社区 2019-04-23

杜雨,EasyCharts团队成员,R语言中文社区专栏作者,兴趣方向为:Excel商务图表,R语言数据可视化,地理信息数据可视化。个人公众号:数据小魔方(微信ID:datamofang) ,“数据小魔方”创始人。


本文将跟大家分享如果在R语言中使用管道操作符优化代码,以及管道函数调用及传参的注意事项。

使用R语言处理数据或者分析,很多时候免不了要写连续输入输出的代码,按照传统书写方式或者习惯,初学者往往会引入一大堆中介变量,或者使用函数嵌套进行一次性输出。

以上两种方法虽然从结果上来看,同样可以达到我们预期的效果,但是无论是代码效率还是内存占用上都存在巨大劣势。

1、使用中介变量会使得内存开销成倍增长,特别是你的原始数据量非常大而内存又有限,在一个处理过程中引入太多中介对象,不仅代码冗余,内存也会迅速透支。

2、使用函数嵌套则避免了内存占用的问题,但是嵌套太多层函数,会造成代码难以理解,阅读困难,甚至给今后的项目复用造成很大的困扰。

而R语言大佬们很早就已经意识到这个问题,开始在R语言中引入管道操作符函数,进行连续传参,实现了内存节省、代码优化的需求。

通常我们使用最多的管道函数来自于magrittr包,该包中管道操作函数写作%>%,这是一个在R语言中使用非常频繁的函数,很多比较成熟的项目扩展包都已经实现了管道操作函数的内置。(比如dplyr、rvest、leaflet等都实现了默认调用)。

在大多数并没有默认加载magrittr包的扩展包函数中使用管道操作符,需要先加载该包之后才能使用该函数。

这里仅以一个小案例来开始今天的讲解:

library(“rvest”)
library(“stringr”)

url<-“http://www.zyzw.com/twzs010.htm“

在不久前的一篇关于中国世界文遗产仪表盘的案例中,我在目标网站上抓取了52个中国世界自然文遗产的名称。

按照传统的引入中间变量的写法,代码应该是这样的:

web<-read_html(url,encoding="GBK") web1<-html_nodes(web,"b") content1<-html_text(web1,trim = FALSE) content2<-gsub("(\\n\\t|,|\\d|、)","",content1) content3<-grep("\\S",content2,value=T) content4<-str_trim(content3,side="both") content5<-content4[1:54] content6<-content5[setdiff(1:54,c(35,39))];content6

一共写了8行代码,引入了7个中介变量,浪费了大量内存,整个代码看着也很辣眼睛。

加入函数嵌套的写法,以上代码可以写成下面的模式:

content<-str_trim(grep("\\S",gsub("(\\n\\t|,|\\d|、)","",html_text(html_nodes(read_html(url,encoding="GBK") ,"b"),trim = FALSE)),value=T),side="both") content<-content[1:54] content<-content[setdiff(1:54,c(35,39))];content

函数嵌套确实省去了不少代码(其实并没有节省多少,充其量是节省了几个中介变量的名称而已,大量的代码全都嵌套在首句里面了),但是这样风格的代码如何保障一眼就看清楚内部的逻辑。

Name<-read_html(url,encoding="GBK") %>%       #读取url所在的目标网页        html_nodes("b") %>%                   #选择b节点内容        html_text(trim = FALSE) %>%           #获取b节点内的文本(清除空格)        gsub("(\\n\\t|,|\\d|、)","",.) %>%   #替换掉文本内的所有制表符、标点符号等        grep("\\S",.,value=T) %>%             #筛选出非空文本        str_trim(side="both") %>%             #清除掉文本两侧的空格        .[1:54]%>%                            #保留字符串向量的1:54个观测值        .[setdiff(1:54,c(35,39))]             #剔除掉其中的第35、39个观测值

以上代码使用管道操作函数依次将左侧独享作为参数传入右侧函数内部,层层传递,不创建任何中间变量,因而这一段代码自url输入起始,到setdiff筛选完毕之后输出NAME终止,没有生成任何中间变量(也就意味着没有浪费任何多余内存)。从代码的简介与优雅程度来看,它也完胜前两者,因为每一句功能都可以通过%>%看到明显的输入输出,当你回看或者修改时,仅需定位到对应代码块调试即可。

以上就是%>%的用法,用一个图示来表示,可以将其表示如下:

实际上在参数传递的细节上,还有很多需要注意的地方:

1、当函数仅需一个必要参数时,则此时函数写法容忍度非常高,相对自由:

sample(letters[1:5],size=20,replace=TRUE) %>% table()   sample(letters[1:5],size=20,replace=TRUE) %>% table sample(letters[1:5],size=20,replace=TRUE) %>% table(.) a b c d 5 7 1 7

以上三种写法都可以输出正确的结果,第一种写法保留了括号,第二种写法省略了括号,第三种同时保留了括号和占位符“.”。

因为table只接受一个位置参数(你也可以理解为必备参数,该参数是一个因子或者类别型变量),从左侧由管道操作符传入的参数就会被作为table的必备参数。
前两种写法等价,第三种写法在括号内加入了占位符,这种用法接下来会讲到。

2、当函数有一个以上的必备参数(位置参数)时,而且管道函数传入的参数位于第一个时,可以写成如下模式:

url %>% read_html(encoding="GBK") url %>% read_html(.,encoding="GBK")

read_html函数中仅有一个位置参数x(必备参数),encoding是一个默认参数,options也是一个默认可选参数。因而x是必备参数,且无需声明参数名称。url被%>%传入read_html之后,默认就被作为x参数的对象。以上两种写法等价,第二种写法声明了x参数在read_html()函数内的位置。(相当于x参数的占位符),但是在此种情况下并不必要。(因为x作为第一个位置参数,可以被默认识别出来)

3、当函数有不止一个位置参数(必备参数)时,且左侧传入的对象在右侧函数中不是位置排在第一个的,那么此种情况下必须显式声明该参数在右侧函数中所处的位置,并且使用“.”作为占位符占位。

read_html(url,encoding="GBK") %>% html_nodes("b") %>% html_text(trim = FALSE) %>% gsub("(\\n\\t|,|\\d|、)",""  ) read_html(url,encoding="GBK") %>% html_nodes("b") %>% html_text(trim = FALSE) %>% gsub("(\\n\\t|,|\\d|、)","", ) read_html(url,encoding="GBK") %>% html_nodes("b") %>% html_text(trim = FALSE) %>% gsub("(\\n\\t|,|\\d|、)","",.)

以上代码中,前两个是错误的,最后一个成功了,原因是gsub函数一共有三个位置参数(必备参数),而我们从左侧传入的那个字符串对象,刚好处于第三个位置参数的位置。如果不做显式声明,告诉gsub函数%>%左侧传入对象在右侧函数中的具体位置,则函数无法自动识别。

gsub(pattern, replacement, x, ignore.case = FALSE, perl = FALSE,fixed = FALSE, useBytes = FALSE)

以上三点是基于函数式编程中,位置参数传参必须遵循的规则,其实也是符合现实逻辑的。通过以上三点,我们可以得出的结论:

1、右侧函数仅有一个位置参数(必备参数时),可只写函数名(不用带括号)、也可以写作函数名带双括号,也可以写作函数名+(.)。以上删照片那个方式都是合法的,但是后两种不是必要的,函数可以根据逻辑自动识别。

2、当右侧函数有多个位置参数时,需要视左侧传入的参数在右侧位置参数中的次序而定,倘若刚好位于右侧所有位置参数第一个,则写法也相对灵活,可以直接忽略掉,只指定其他位置参数和默认参数,倘若位于第一个之后,则必须给出精确的显式位置声明,并使用占位符“.”占位。

除此之外,管道函数传参时,也支持传给数据框的切片索引操作。如下所示:

read_html(url,encoding="GBK") %>% html_nodes("b") %>%         html_text(trim = FALSE) %>%  gsub("(\\n\\t|,|\\d|、)","",.) %>%       grep("\\S",.,value=T) %>% str_trim(side="both") %>%          .[1:54]

最后一次传参的时候,左侧传入了一个文本向量,可以像普通场景下的向量下标索引一样对观测值进行过滤,此时左侧向量名称可以不用写出, 用一个占位符替代即可(这里的.必不可少)。

magrittr包为了保证管道函数传参过程更为高效,提供了很多类似%>%的辅助函数:

函数名称:                函数符号表达式: extract                  `[` extract2                 `[[` inset                    `[<-` inset2                   `[[<-` use_series               `$` add                      `+` subtract                 `-` multiply_by              `*` raise_to_power           `^` multiply_by_matrix       `%*%` divide_by                `/` divide_by_int            `%/%` mod                      `%%` is_in                    `%in%` and                      `&` or                       `|` equals                   `==` is_greater_than          `>` is_weakly_greater_than   `>=` is_less_than             `<` is_weakly_less_than      `<=` not (`n'est pas`)        `!` set_colnames             `colnames<-` set_rownames             `rownames<-` set_names                `names<-`

以上函数中有我们经常用到的四则运算、逻辑判断与比较函数、包含关系函数等,也有一些使用频率不高的冷门函数。我仅取其中常见的几个进行简要介绍。

library(“magrittr”)

extract函数等价于 `[`,用于索引数据框中的列:

iris %>% extract(,1:3)  %>% head iris %>% `[`(1:3)       %>% head iris %>% .[,1:3]        %>% head


以上三种方法索引iris前三列并预览,结果是等价的。

extract2函数等价于`[[`,用于索引列表中的顺序对象。

mydata<-list(         mmm=1:5,         nn=c("dd","fff","ttt"),         dd=6:20         ) mydata %>% extract2(1) mydata %>% `[[`(1) mydata[[1]] mydata$mmm
1 2 3 4 5

以上四种索引方式等价。

good.times<-Sys.Date() %>% as.POSIXct %>% seq(by="15 mins",length.out=100) %>% data.frame(timestamp = .) good.times$quarter<-good.times %>% use_series(timestamp) %>% format("%M")  %>% as.numeric %>% divide_by_int(15) %>% add(1) good.times$quarter<-good.times %>% .[,"timestamp"]       %>% format("%M")  %>% as.numeric %>% `%/%`(15)         %>% `+`(1) [1] 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 [61] 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4

以上两种方式输出结果等价,特别注意最后的%>% `%/%`(15)函数,%>% `+`(1),前者代表左侧传入值除以15的商的整数部分,后者代表向量统一增加1.

add                      `+` subtract                 `-` multiply_by              `*` raise_to_power           `^` multiply_by_matrix      `%*%` divide_by               `/` divide_by_int           `%/%` mod                     `%%`

以上是集中简单几何运算,均可以通过函数名称或者符号表达式的方式结合%>%进行传参,节省代码,提高效率。

至于其他那些尚未讲解到的特殊用法,感兴趣可以参考源文档。

本文参考文献:
https://cosx.org/2014/04/use-pipeline-operators-in-r
http://blog.fens.me/r-magrittr/
https://cran.r-project.org/web/packages/magrittr/magrittr.pdf

往期案例数据请移步本人GitHub:
https://github.com/ljtyduyu/DataWarehouse/tree/master/File

推荐阅读

《R的极客理想》系列图书作者张丹:用R语言把数据玩出花样

kaggle:NBA球员投篮数据分析与可视化

做一个人见人爱的数据分析师,从让销售满意开始

R语言中文社区历史文章整理(类型篇)

怎样才算精通R语言?



公众号后台回复关键字即可学习

回复 R              R语言快速入门免费视频 
回复 统计          统计方法及其在R中的实现
回复 用户画像   民生银行客户画像搭建与应用 
回复 大数据      大数据系列免费视频教程
回复 可视化      利用R语言做数据可视化
回复 数据挖掘   数据挖掘算法原理解释与应用
回复 机器学习   R&Python机器学习入门 

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存